昨天已完成錄音功能的基本架構,今天進入語音功能的實作。
我希望這個功能不只是「錄完一段語音,再上傳檔案進行翻譯」,而是能夠在使用者說話的過程中,就逐步將音訊送出並進行後續處理。本日的重點會放在 Flutter 錄音、音訊切片,以及低延遲傳輸。
錄音部分使用 record 套件,針對 iOS 與 Android 的麥克風權限及平台設定進行處理。錄音資料則會切成約 100~500ms 的小片段,不是等整段錄音結束後才產生完整的 .wav 或 .mp3 檔案,讓後續的語音辨識能夠更即時地進行。最後再搭配 Flutter UI 顯示目前的錄音狀態,完成基本的錄音流程。
在 pubspec.yaml 加入:
dependencies:
flutter:
sdk: flutter
record: ^5.1.2 # 音訊錄製與 Stream 串流套件
permission_handler: ^11.3.1 # 麥克風權限管理
執行 flutter pub get
android/app/src/main/AndroidManifest.xml)<uses-permission android:name="android.permission.RECORD_AUDIO" />
ios/Runner/Info.plist)<key>NSMicrophoneUsageDescription</key>
<string>需要麥克風權限以進行語音輸入與 AI 對話</string>
建立實作即時語音與串流監聽:
import 'dart:async';
import 'dart:typed_data';
import 'package:record/record.dart';
import 'package:permission_handler/permission_handler.dart';
class AudioRecorderService {
final AudioRecorder _audioRecorder = AudioRecorder();
StreamSubscription<Uint8List>? _audioStreamSubscription;
// 1. 檢查並請求麥克風權限
Future<bool> requestPermission() async {
final status = await Permission.microphone.request();
return status.isGranted;
}
// 2. 開始錄音並進行 Chunking 串流切片
Future<void> startChunkedRecording(Function(Uint8List chunk) onAudioChunkReceived) async {
final hasPermission = await requestPermission();
if (!hasPermission) {
print('❌ 麥克風權限被拒絕');
return;
}
// 設定錄音規格(低延遲通常使用 PCM 16bit 16kHz Mono)
const config = RecordConfig(
encoder: AudioEncoder.pcm16bits, // 原始音訊格式,適合即時 AI 處理
sampleRate: 16000, // 16kHz 採樣率(Whisper / 語音模型常用)
numChannels: 1, // 單聲道
);
// 取得即時音訊數據串流 (Audio Stream)
final Stream<Uint8List> audioStream = await _audioRecorder.startStream(config);
// 監聽串流,每當產生新的 Audio Chunk 就回傳
_audioStreamSubscription = audioStream.listen((Uint8List chunk) {
print('🎙️ 收到 Audio Chunk 大小: ${chunk.length} bytes');
onAudioChunkReceived(chunk); // 將 chunk 送給外部(如 WebSocket)
});
}
// 3. 停止錄音
Future<void> stopRecording() async {
await _audioStreamSubscription?.cancel();
_audioStreamSubscription = null;
await _audioRecorder.stop();
print('🛑 錄音已停止');
}
// 4. 釋放資源
void dispose() {
_audioRecorder.dispose();
}
}
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'services/audio_recorder_service.dart';
class AudioRecorderPage extends StatefulWidget {
const AudioRecorderPage({super.key});
@override
State<AudioRecorderPage> createState() => _AudioRecorderPageState();
}
class _AudioRecorderPageState extends State<AudioRecorderPage> {
final AudioRecorderService _recorderService = AudioRecorderService();
bool _isRecording = false;
int _totalBytesReceived = 0;
void _toggleRecording() async {
if (_isRecording) {
await _recorderService.stopRecording();
setState(() {
_isRecording = false;
});
} else {
setState(() {
_totalBytesReceived = 0;
_isRecording = true;
});
await _recorderService.startChunkedRecording((Uint8List chunk) {
// 收到新的音訊切片(Chunk)
setState(() {
_totalBytesReceived += chunk.length;
});
});
}
}
@override
void dispose() {
_recorderService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Day 9 - 音訊切片實作')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_isRecording ? Icons.mic : Icons.mic_none,
size: 80,
color: _isRecording ? Colors.red : Colors.grey,
),
const SizedBox(height: 20),
Text(
_isRecording ? '正在即時錄音與切片中...' : '點擊下方按鈕開始錄音',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 10),
Text(
'累計接收音訊資料: $_totalBytesReceived bytes',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: _toggleRecording,
style: ElevatedButton.styleFrom(
backgroundColor: _isRecording ? Colors.red : Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: Text(
_isRecording ? '停止錄音' : '開始錄音',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
],
),
),
);
}
}
完成後,要在Console(偵錯主控台)看到以下輸出:
🎙️ 收到 Audio Chunk 大小: 1280 bytes

看到有以上截圖內容代表成功了!
為什麼不存成 .wav 或 .mp3 檔案再傳?
如果存成檔案才上傳,使用者必須講完話、停止錄音後才能發送,延遲會長達數秒,所以我們採用串流(Stream)方式傳送音訊資料,將錄音過程中取得的音訊資料分段即時傳送給後端/Whisper API,讓語音辨識與翻譯可以在使用者說話的同時進行,降低整體延遲,實現接近即時的翻譯效果。